package clstatdb;

use clmasterdefaults;
use C_client2;
use C_server2;
use Sys::Hostname;
use DBI;
use Data::Dumper;
use Time::HiRes qw(time alarm sleep);
use MIME::Base64;
use POSIX ();
use Carp qw(cluck);

#########################################################################
# Class level variables to record errors of last call.			#
#########################################################################

my $cldb_errcode=0;
my $cldb_errstr=undef;

#########################################################################
# Records is an array of known record types with details of each field	#
# including whether it is optional or not, and what kind of validation	#
# and manipulation to apply when attempting to add records via the	#
# add_verified_record method.						#
#########################################################################

$::STATDB_NPORT=11884;
{
	my $defaults=new clmasterdefaults("$::ENV{TRUECL_CFG}/master-defaults.xml");
	if(!defined($defaults)) {
		print STDERR "ERROR: ",clmasterdefaults::error(),"\n";
	}
	my $xx=$defaults->get_default_value("statdb_port",1);
	if(defined($xx)) {
		$::STATDB_NPORT=$xx;
	}
}

my %records=(
	"node_status" => {
		"node"		=> ["m",\&is_unique],
		"update_time"	=> ["m",\&is_integer]
		},

#	"ports"	=> {
#		"number"	=> ["m",\&is_unique,\&is_integer,\&is_nowhitespace],
#		"type"		=> ["m"],
#		"detail"	=> ["o"],
#		},
#	"topology" => {
#		"name"		=> ["m",\&is_unique,\&is_nowhitespace],
#		"lcards"	=> ["o"],
#		"pcards"	=> ["o"],
#		"type"		=> ["m",\&is_valid_nettype]
#		},
#	"applications" => {
#		"name"		=> ["m",\&is_unique_app_in_cluster],
#		"prefnode"	=> ["m",\&is_valid_app_preference],
#		"nodes"		=> ["m",\&is_valid_node_list],
#		"consistency"	=> ["m",\&is_valid_failover_type],
#		"startscript"	=> ["o",\&is_valid_script],
#		"stopscript"	=> ["o",\&is_valid_script],
#		"starttimeout"	=> ["o",\&is_integer],
#		"stoptimeout"	=> ["o",\&is_integer],
#		"checksum"	=> ["m",\&is_sha1],
#		},
#	"application_ips" => {
#		"name"		=> ["m",],
#		"network"	=> ["m",],
#		"ips"		=> ["m",],
#		"netmask"	=> ["o",\&is_netmask],
#		"broadcast"	=> ["o",\&is_ip_addr],
#		"version"	=> ["o",\&is_4_or_6],
#		},
#	"application_fs" => {
#		"name"		=> ["m",\&is_cluster_app],
#		"mountpnt"	=> ["m"],
#		"options"	=> ["o"],
#		"fstype"	=> ["m",\&is_supported_fs],
#		"vg"		=> ["m",\&is_existing_vg,\&is_unique_in_cluster],
#		"lv"		=> ["m",\&is_existing_lv],
#		"size"		=> ["m",\&is_integer],
#	},
#	"locks" => {
#		"name"		=> ["m",\&is_unique,\&is_nowhitespace],
#		"type"		=> ["m",\&is_host_or_cluster],
#		"desc"		=> ["m"],
#		"maxtime"	=> ["m",\&is_integer],
#		"maxkeepers"	=> ["m",\&is_integer],
#	}
);

my %records_postadd=(
);

{
my $dd=0;
my $debugmode=0;

sub _dprint {
	if(time-$dd > 60) {
		if(-f "$::ENV{TRUECL_CODE}/flags/clstatdb.debug") {
			$debugmode=1;
		} else {
			$debugmode=0;
		}
		$dd=time;
	}
	if($debugmode) {
		my $fd;
		open($fd,">>$::ENV{TRUECL_CODE}/log/clstat-debug.log");
		print $fd @_;
		close($fd);
	}
	if($::DEBUG) {
		print STDERR @_;
	}
}

}

sub _start_n_handle_port($$) {
my $port=shift;
my $dbname=shift;
my $sub_service=undef;
my $hostname=hostname();

	_dprint "$$: Attempt to check for service on $port...\n";
	# Attempt to open service, abort if error
	my $client=new C_client2(SERVER_NAME => $hostname,SERVER_INET => $hostname,
			SERVER_PORT=>$port, PKI=>$::PKI);
	my ($resp,$err);
	if(defined($client)) {
		$client->set_params(TIMEOUT => 10);
		# Attempt a ping and if ok return 2 - already present.
		($resp,$err)=$client->send_request("PING");
		if(defined($resp) && $resp =~ /^OK/) {
			_dprint "$$: Service on port $port found.\n";
			return (3,$client); # service running - client
		} elsif(!defined($resp) && defined($err) && $err =~ /PROTOCOL/) {
			_dprint "$$: Protocol error: $err\n";
			return(0,$err);
		} elsif(!defined($resp)) {
			_dprint "$$: No response in timeout of 10 seconds - assuming port not available.\n";
			_dprint "$:: Error: $err\n" if defined($err);
		} else {
			_dprint "$:: Error: $err\n" if defined($err);
			_dprint "$$: Unexpected response: $resp\n";
		}
	}
	# Attempt to start service - it might already be running, 
	# and so attempt ping and drop through if so.
	_dprint "$$: No service found, attempting to start...\n";
	my $chld=fork();
	if(!defined($chld)) {
		_dprint "$$: Service could not start - fork failed. :(\n";
		return(0,undef);
	}
	if($chld==0) {
		$0="lha_statdb_service";
		# close all open file descriptors above 2..
		POSIX::close($_) foreach(4 .. 1024); # 3 is log
		$sub_service=new C_server2(PKI => $::PKI,PORT => $port);
		if(!$sub_service) {
			_dprint "$$: No service started, possibly started already...\n";
			exit(0);
		}
		_dprint "$$: Service start on pid $$.\n";
		handle_statdb_requests($sub_service,$dbname,$port);
	}
	for(1..10) {
		select(undef,undef,undef,0.25);
		# Attempt to open service, abort if error
		$client=new C_client2(SERVER_NAME => $hostname,SERVER_INET => $hostname,
				SERVER_PORT=>$port, PKI=>$::PKI);
		if(!defined($client)) {
			error("Could not define clstatdb service, or connect to it!",1);
		}
		$client->set_params(TIMEOUT => 10);
		# Attempt a ping and if ok return 2 - already present.
		($resp,$err)=$client->send_request("PING");
		if(defined($resp) && $resp =~ /^OK/) {
			_dprint "$$: Service on port $port found on attempt after failed server create.\n";
			return (3,$client); # service running
		}
	}
	_dprint "$$: Service could not start. :(\n";
	return (0,undef); # service not running, and could not start it
}

{
my $KEEP_ON_RUNNING=1;
my $DO_RECONFIGURE=0;
my $server=undef;
my $dbh=undef; # peristent database connection for performance
my $dbname=undef; # name of database to connect to
my $port=undef;

sub reconfigure($) {
my $server=shift;
	$server->close();
	POSIX::close($_) foreach(4 .. 1024); # 3 is log
	my $sub_service=new C_server2(PKI => $::PKI,PORT => $port);
	if(!$sub_service) {
		_dprint "$$: No service started, possibly started already...\n";
		exit(0);
	}
	_dprint "$$: Service restart on pid $$.\n";
	$sub_service->load_window_keys();
	$::SIG{USR1} = sub {$DO_RECONFIGURE=1;};
	return $sub_service;
}

sub handle_statdb_requests($$$) {
$server=shift;
$dbname=shift;
$port=shift;

	$0="lha_statdb_service";
	$::SIG{HUP} = sub {$KEEP_ON_RUNNING=1;}; # ignore and if parent dies
	$::SIG{INT} = sub {$KEEP_ON_RUNNING=0;};
	$::SIG{USR1} = sub {$DO_RECONFIGURE=1;};

	$dbh=DBI->connect("dbi:SQLite:dbname=$dbname","","",
			{
				AutoCommit => 1,
				RaiseError => 0,
				PrintError => 0,
			});
	if(!defined($dbh)) {
		::error("Unable to connect to database [$dbname] ... exiting status db sub-service.",1);
	}
	
	my $start_time=time;
	my $run_until=$start_time+2*24*60*60;
	while($KEEP_ON_RUNNING) {
		my ($request,$err)=$server->accept(2);
		last if ! $KEEP_ON_RUNNING;
		if($DO_RECONFIGURE) {
			$server=reconfigure($server);
			$DO_RECONFIGURE=0;
		}
		if(!defined($request)) {
			if(time > $run_until) {
				::msg("Status database servicing finishing for now.");
				exit(0);
			}
			# Timeout - so check locks and loop around again.	#
			next;
		}
		if($err) {
			error($err);
			next;
		}

		#################################################################
		# Handle SET_LOG_LEVEL silent|quiet|verbose|debug		#
		#################################################################
		if($request =~ /^SET_LOG_LEVEL\s+(\S+)/i) {
			my $lev=lc($1); my $ok=0;
			if($lev eq "silent")  { $ok=1; $::DEBUG=0; $::LOGGER->set_silent(); }
			if($lev eq "quiet")   { $ok=1; $::DEBUG=0; $::LOGGER->set_quiet(); }
			if($lev eq "verbose") { $ok=1; $::DEBUG=0; $::LOGGER->set_verbose(); }
			if($lev eq "debug")   { $ok=1; $::DEBUG=1; $::LOGGER->set_debug(); }
			if(! $ok) {
				$server->send_response("ERROR Log level '$lev' not recognised.");
			} else {
				$server->send_response("OK");
			}
			next;
		}

		my @F=split(/\s+/,$request);
		if($F[0] eq "ABORT") {
			$server->send_response("OK"); # Safe even if closed.
			last;
		}
		if($F[0] eq "PING" || $F[0] eq "ECHO") {
			$server->send_response("OK"); # Safe even if closed.
			next;
		}
		# Now the real reason for having this daemon, do() and getrows
		# handling... 
		# DO 0|1 REQUESTS
		# Each request is base64 encoded with "|" separating multiple
		# statements. A begin/end transaction is placed around all the
		# statements. A count of successful statements is returned, as 
		# in "OK 22 FAIL 0 b64errs".
		# If error it will abort straight away if ALL=0 (or not present).
		# If ALL=1 (first arg) keeps trying other statements too.
		# Return is "OK nn FAIL nn" again.
		# If errors, then b64errs is base64 encoded errors with "|"
		# between them,
		#
		# GET MAX SQL
		# MAX is count of maximum records to return, all matches if 0.
		# SQL is base64 encoded sql statement (optional ; at end).
		# Return values are:
		# FAIL error_msg
		# OK NN B64recoreds
		# The return value is a base64 encoded array of hashes generated 
		# via data::dumper. So eval it to get VAR1 as array reference.

		if($F[0] eq "DO") {
			if(@F!=3) {
				$server->send_response("ERROR Invalid request syntax");
				next;
			}
			$server->send_response(handle_do_request($F[1],$F[2]));
			next;
		}
		if($F[0] eq "GET") {
			if(@F!=3) {
				$server->send_response("ERROR Invalid request syntax");
				next;
			}
			$server->send_response(handle_get_request($F[1],$F[2]));
			next;
		}
		$server->send_response("ERROR Request not recognised");
	}
	::msg("Aborting on user request.");
	exit 0;
}

sub handle_do_request($$) {
my $all=shift;
my $statements=shift;

	my @S=split(/\|/,$statements);
	my $ok=0;
	my $fail=0;

	my @E=();
	for my $xx (@S) {
		my $do=decode_base64($xx);
		_dprint "$$: handle_do_request SQL=$do\n";
		$do =~ s/\s+$//;
		$do =~ s/^\s+//;
		$do =~ s/;$//;
		$do.=";";
		# ok so do is a string, so attempt to perform it...
		$dbh->{AutoCommit}=1;
		$dbh->begin_work;
		$dbh->do($do);
		$dbh->commit;
		if(defined($dbh->err)) {
			$fail++;
			push @E,$dbh->errstr;
			_dprint "$$: handle_do_request ERROR=",$dbh->errstr,"\n";
			if(! $all) {
				last;
			}
		} else {
			$ok++;
		}
	}
	my $err_str="";
	if(@E) {
		my @E2=();
		for my $ce (@E) {
			push @E2,encode_base64($ce,"");
		}
		$err_str=join("|",@E2);
		_dprint "$$: handle_do_request RETURN=OK $ok FAIL $fail $err_str\n";
		return "OK $ok FAIL $fail $err_str";
	}
	_dprint "$$: handle_do_request RETURN=OK $ok FAIL $fail\n";
	return "OK $ok FAIL $fail";
}

sub handle_get_request($$) {
my $max=shift;
my $statement=shift;

	_dprint "$$: Entered handle_get_request.\n";
	my $cd=$Data::Dumper::Indent;
	$Data::Dumper::Indent=0;
	my $do=decode_base64($statement);
	$do =~ s/\s+$//;
	$do =~ s/^\s+//;
	$do =~ s/;$//;
	$do.=";";
	_dprint "$$: SELECT='$do'\n";

	my $sth;
	eval {
		$sth=$dbh->prepare($do);
	};
	if($@ || !defined($sth)) {
		$rc=$dbh->errstr;
		_dprint "$$: Failed to prepare SQL: $rc\n";
		return "FAIL $rc";
	}
	eval {
		$sth->execute();
	};
	if($@) {
		$rc=$dbh->errstr;
		_dprint "$$: Execute failed: $rc\n";
		return "FAIL $rc";
	}
	@R=();
	my $cc=0;
	$max=99999 if $max<1;
	eval {
		while(my $aref  = $sth->fetchrow_hashref) {
			push @R,{%$aref};
			$cc++;
			last if $cc>$max;
		}
	};
	if($@) {
		$rc="Invalid SQL or no such table.";
		_dprint "$$: Fetchrow failed: $rc\n";
		return "FAIL $rc";
	}

	_dprint "$$: Grabbed $cc records.\n";
	$res=encode_base64(Dumper(\@R),"");
	_dprint "$$: Returning: 'OK $cc $res'\n";
	return "OK $cc $res";
}

}

sub new {
my $proto = shift;
my $class = ref($proto) || $proto;
my $self  = {};
my $dbname=shift;
my %extra=@_;

	# Attempt a connection to the sub-service...

	if(!defined($dbname)) {
		$dbname=$::ENV{TRUECL_CFG}."/status.db";
	}
	if(exists($extra{PORT})) {
		$self->{PORT}=$extra{PORT};
	} else {
		$self->{PORT}=$::STATDB_NPORT;
	}
	my ($rc,$service)=_start_n_handle_port($self->{PORT},$dbname);
	if($rc==3) {
		$self->{client}=$service;
		bless($self,$class);
		return $self;
	}
	if($rc==0) {
		$cldb_errcode=1;
		if(!defined($service)) {
			$cldb_errstr="Failed to start service for Status DB on port $self->{PORT}.";
		} else {
			$cldb_errstr=$service;
		}
		return undef;
	}
	die "Unexpected return code from _start_n_handle_port.";
}

#########################################################################
# Class method to return the latest error (code,string), 		#
# or undef if last call was successful.					#
#########################################################################

sub error {
	return($cldb_errcode,$cldb_errstr);
}

#########################################################################
# The force_close actually just causes the sub-process to quit. This	#
# will obviously result in the database being written out to disk.	#
#########################################################################

sub force_close($) {
my $self=shift;

	my $client=$self->{client};
	my ($resp,$err)=$client->send_request("ABORT");
	select(undef,undef,undef,1);
	return 1;
}

sub abort() {
my $self=shift;

	my $client=$self->{client};
	my ($resp,$err)=$client->send_request("ABORT");

	select(undef,undef,undef,0.5);

	# If we abort, look for new port possibly and use that
	# for client connections ...
	my $defaults=new clmasterdefaults("$::ENV{TRUECL_CFG}/master-defaults.xml");
	if(!defined($defaults)) {
		print STDERR "ERROR: ",clmasterdefaults::error(),"\n";
	}
	my $xx=$defaults->get_default_value("statdb_port",1);
	if(defined($xx)) {
		$::STARTB_NPORT=$xx;
	}
}

#########################################################################
# Instance method to execute any old SQL against the current database.	#
#########################################################################

sub do($$;$) {
my $self=shift;
my $sql=shift;
my $timeout=shift;

	$timeout=3 if !defined($timeout);
	$cldb_errcode=0;
	$cldb_errstr=undef;
	my $client=$self->{client};
	my @do_sql=();
	if(ref($sql)) {
		push @do_sql,$_ foreach (@$sql);
	} else {
		push @do_sql,$sql;
	}
	my @B=();
	for my $c (@do_sql) {
		push @B,encode_base64($c,"");
		_dprint "$$: Attempting: \"$c\"\n";
	}
	my $s=join("|",@B);
	my $cmd="DO 1 $s";
	
	my ($resp,$err)=$client->send_request($cmd);
	if(!defined($resp)) {
		$cldb_errcode=2;
		$cldb_errstr="Failed to get response from db service.";
		return 0;
	}
	_dprint "$$: DO 1 result is: $resp\n";
	if($resp !~ /^OK/) {
		$resp =~ s/^(ERROR|FAIL)\s*//;
		$cldb_errcode=1;
		$cldb_errstr=$resp;
		return 0;
	}
	my @S=split(/\s+/,$resp);
	$cldb_errcode=$S[3];
	if(@S==5) {
		$cldb_errstr=decode_base64($S[4]); # errors, if present..
	}
	return $S[1]; # count of ok
}

sub get_rows($$) {
my ($self,$query)=@_;

	$cldb_errcode=0;
	$cldb_errstr=undef;
	my $client=$self->{client};
	my $cmd="GET 0 ".encode_base64($query,"");
	my ($resp,$err)=$client->send_request($cmd);
	if(!defined($resp)) {
		$cldb_errcode=1;
		$cldb_errstr=$err;
		return [];
	}
	_dprint "$$: get_rows GET 0 resp=$resp\n";
	if($resp !~ /^OK/) {
		$resp =~ s/^(ERROR|FAIL)\s*//;
		$cldb_errcode=1;
		$cldb_errstr=$resp;
		return [];
	}
	my $VAR1;
	my @S=split(/\s+/,$resp);
	my $r=decode_base64($S[2]);
	_dprint "$$: Decoded get_rows response to:\n",Dumper($r);
	eval $r;
	return $VAR1;
}

# Does nothing, since autocommit is now turned on

sub commit($) {
my $self=shift;

	$cldb_errcode=0;
	$cldb_errstr=undef;
}

sub disconnect {
my $self=shift;

	# Actually we do nothing; since persistent..
	_dprint("$$: Closed database.\n");
	return;
}

sub DESTROY {
my $self=shift;

	_dprint("$$: Closed database[in DESTROY].\n");
}

sub exists($$$) {
my $self=shift;
my ($type,$obj)=@_;

	$cldb_errcode=0;
	$cldb_errstr=undef;
	$type="table" if !defined($type);
	my $cmd="SELECT * FROM SQLITE_MASTER WHERE type = '$type' AND name = '$obj';";

	# Connect service service to get records
	my $recs=$self->get_rows($cmd);
	if(@$recs==0) {
		return 0;
	}
	return 1;
}

sub get_object_schema($$$) {
my ($self,$type,$obj)=@_;

	$cldb_errcode=0;
	$cldb_errstr=undef;
	if(!defined($obj)) {
		$cldb_errcode=6;
		$cldb_errstr="Specified object value is undefined.";
		return undef;
	}
	$type="table" if !defined($type);
	my $cmd="SELECT * FROM SQLITE_MASTER WHERE type = '$type' AND name = '$obj';";

	# Connect service service to get records
	my $recs=$self->get_rows($cmd);
	if(@$recs==0) {
		return 0;
	}
	if(@$recs==0) {
		$cldb_errcode=8;
		$cldb_errstr="$obj of type $type does not exist.";
		return undef;
	}
	return $recs->[0]->{sql};
}

sub get_record_requirements {
my ($crec,$str);

	$str="";
	for $crec (sort(keys %records)) {
		$str.=sprintf("  * %-20s ::= ",$crec);
		my $ckey;
		for $ckey (keys(%{$records{$crec}})) {
			if($records{$crec}->{$ckey}->[0] eq "m") {
				$str.="$ckey ";
			}
		}
		for $ckey (keys(%{$records{$crec}})) {
			if($records{$crec}->{$ckey}->[0] eq "o") {
				$str.="[$ckey] ";
			}
		}
		$str.="\n";
	}
	return $str;
}

#########################################################################
# If the specified table is known, but does not yet exist, then create	#
# it as necessary.							#
#########################################################################

sub create_if_needed($$) {
my $self=shift;
my $ARG_table=shift;
my %T=(
	"topology_status"	=> "CREATE TABLE topology_status(topology VARCHAR(50),failcount INTEGER, lastfailtime CHAR(20), nextreset INTEGER, card CHAR(20), status INTEGER, alive INTEGER);",
	"app_route_status"	=> "CREATE TABLE app_route_status(label VARCHAR(50),appname VARCHAR(50),failcount INTEGER, lastfailtime CHAR(20), nextreset INTEGER, status INTEGER, alive INTEGER);",
	"restart_apps"		=> "CREATE TABLE restart_apps(appname VARCHAR(50),failnode VARCHAR(50), failtime INTEGER);",
	"modded_settings"	=> "CREATE TABLE modded_settings(appname VARCHAR(50),attrname VARCHAR(50), value VARCHAR(255));",
	"app_status"		=> "CREATE TABLE app_status(name VARCHAR(50) PRIMARY KEY,host VARCHAR(50), status VARCHAR(20), starttime INTEGER, updatetime INTEGER, valid VARCHAR(200));",
	"app_registered"	=> "CREATE TABLE app_registered(name VARCHAR(50) PRIMARY KEY, date INTEGER );",
	"cluster_status_vars"	=> "CREATE TABLE cluster_status_vars(var VARCHAR(50) PRIMARY KEY, value VARCHAR(200) );",
	"data_sync"		=> "CREATE TABLE data_sync(appname VARCHAR(50), lvname VARCHAR(50), vgname VARCHAR(50), updatetime INTEGER, status CHAR(10), storage_spec VARCHAR(50), attempts INTEGER, req_syncrate INTEGER, act_syncrate INTEGER);",
	"app_ips"		=> "CREATE TABLE app_ips(appname VARCHAR(50), network VARCHAR(50), ind INTEGER, ip  VARCHAR(20), updatetime INTEGER, type CHAR(2), netmask VARCHAR(20), broadcast VARCHAR(20));",
	"diskhb"		=> "CREATE TABLE diskhb(diskhb VARCHAR(50), status VARCHAR(50));",
	"mond_status"	=> "CREATE TABLE mond_status(type CHAR(1), appname VARCHAR(50),monname VARCHAR(50), montype VARCHAR(50), status INTEGER, interval INTEGER, failcount INTEGER, lastfailtime INTEGER, wakeup INTEGER, rearm INTEGER, pid INTEGER);"
	);

	if(!exists($T{$ARG_table})) {
		return 0;
	}
	if(!clstatdb::exists($self,"table",$ARG_table)) {
		my $r=$self->do($T{$ARG_table});
		if(!$r) {
			return 0;
		}
	}
	return 1;
}

1;

